package com.youlu.campus.service.clock;

import cn.hutool.core.date.LocalDateTimeUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.youlu.campus.base.exception.BusinessException;
import com.youlu.campus.entity.*;
import com.youlu.campus.entity.clock.UserClockRecord;
import com.youlu.campus.entity.clock.dto.UserClockQueryDTO;
import com.youlu.campus.entity.clock.dto.UserClockSaveDTO;
import com.youlu.campus.entity.clock.dto.UserSignInfoDTO;
import com.youlu.campus.entity.clock.vo.UserClockRecordVo;
import com.youlu.campus.entity.enums.TaskBizType;
import com.youlu.campus.service.activity.ActivityTaskService;
import com.youlu.campus.service.activity.UserParticipateActivityInfoService;
import com.youlu.campus.service.invite.UserInviteRecordService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.*;

/**
 * @author : [dengbo]
 * @className : UserClockService
 * @description : [用户打卡服务]
 * @createTime : [2022/7/6 15:41]
 */
@Service
@Slf4j
public class UserClockService {
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private ActivityTaskService activityTaskService;
    @Autowired
    private UserParticipateActivityInfoService userParticipateActivityInfoService;
    @Autowired
    private UserInviteRecordService userInviteRecordService;
    @Autowired
    private ActivityClockRedisService activityClockRedisService;

    /**
     * 用户打卡
     *
     * @param dto
     * @return
     */
    public boolean userClock(UserClockSaveDTO dto) {
        String userId = dto.getUserId();
        String activityId = dto.getActivityId();
        LocalDateTime currentTime = dto.getClockDate();
        LocalDate date = currentTime.toLocalDate();
        ActivityTaskInfo activityTaskInfo = activityTaskService.find(activityId, TaskBizType.LEARNING);
        if (activityTaskInfo == null) {
            throw new BusinessException("未配置签到任务");
        }
        //获取用户报名信息
        UserParticipateActivityInfo participateActivityInfo = userParticipateActivityInfoService.findByUserIdActivityId(userId, activityId);
        // 查询本周签到信息
        if (participateActivityInfo == null || !participateActivityInfo.getJoined()) {
            throw new BusinessException("用户未报名");
        }
        Date signDate = participateActivityInfo.getSignUpTime();
        LocalDate signLocalDate = LocalDateTimeUtil.of(signDate).toLocalDate();
        int diff = getOffset(signLocalDate, date);
        if (diff < 0) {
            throw new BusinessException("活动未开始");
        }
        String signKey = activityClockRedisService.getUserActivityClockKey(activityId, userId, true);
        boolean re = activityClockRedisService.getSign(signKey, diff);
        if (re) {
            throw new BusinessException("已经打卡了");
        }
        boolean isFirst = activityClockRedisService.getSignCount(signKey) == 0;
        activityClockRedisService.sign(signKey, diff);
        // 添加打卡记录
        UserClockRecord record = buildRecord(dto, currentTime);
        record.setSignDate(date);
        record.setTaskId(activityTaskInfo.getId());
        // 获取连续打卡天数
        int continueDay = diff + 1;
//        long signNum = getSignNum(signKey, 0, continueDay);
//        int continueSignDay = getSignContinueCount(signNum, continueDay, true);
        int continueSignDay = doLoopSignContinue(continueDay, signKey,true);
        record.setDayContinue(continueSignDay);
        mongoTemplate.insert(record);
        if (isFirst) {
            addInviteClockInfo(activityId, userId, activityTaskInfo.getId());
        }
        return true;
    }

    /**
     * 添加打卡邀请
     *
     * @param activityId
     * @param userId
     * @param taskId
     */
    public void addInviteClockInfo(String activityId, String userId, String taskId) {
        // 1、查询邀请记录
        UserInviteRecord userInviteRecord = userInviteRecordService.invitedSuccessByInvitedToUserIdAndActivityId(userId, activityId);
        if (userInviteRecord == null || StringUtils.isBlank(userInviteRecord.getExtra())) {
            return;
        }
        // 2、查询补卡日期
        log.info("invite param:{}", userInviteRecord.getExtra());
        JSONObject object = JSONObject.parseObject(userInviteRecord.getExtra());
        String signDateStr = object.getString("signDate");
        if (StringUtils.isBlank(signDateStr)) {
            return;
        }
        String inviteUser = userInviteRecord.getInvitedByUserId();
        // 3、获取邀请人报名信息
        UserParticipateActivityInfo participateActivityInfo = userParticipateActivityInfoService.findByUserIdActivityId(inviteUser, activityId);
        if (participateActivityInfo == null || !participateActivityInfo.getJoined()) {
            return;
        }
        // 4、判断是否在正常时间内补卡
        Date joinDate = participateActivityInfo.getSignUpTime();
        LocalDate joinLocalDate = LocalDateTimeUtil.of(joinDate).toLocalDate();
        LocalDate reSignDate = getDateByStr(signDateStr);
        LocalDate currentDate = LocalDate.now();
        int diff = getOffset(reSignDate, currentDate);
        if (reSignDate.isBefore(joinLocalDate) || diff <= 0) {
            return;
        }
        //5、邀请人是否补过卡了
        boolean isReClock = userReClock(participateActivityInfo, joinLocalDate, reSignDate, taskId);
        if (isReClock) {
            log.info("user:{} has re clock", inviteUser);
            return;
        }
        // 更新邀请用户连续打卡数据
        UserClockRecord currentDayRecord = find(activityId, inviteUser, currentDate);
        if (currentDayRecord != null) {
            int di = getOffset(joinLocalDate, currentDate);
            int continueDay = di + 1;
            String signKey = activityClockRedisService.getUserActivityClockKey(activityId, userId, true);
            long signNum = getSignNum(signKey, 0, continueDay);
            int userContinueSignDay = getSignContinueCount(signNum, continueDay, true);
            currentDayRecord.setDayContinue(userContinueSignDay);
            mongoTemplate.save(currentDayRecord);
        }
    }

    /**
     * 添加邀请补卡
     *
     * @param participateActivityInfo
     * @param joinLocalDate
     * @param reSignDate
     * @param taskId
     * @return
     */
    public boolean userReClock(UserParticipateActivityInfo participateActivityInfo, LocalDate joinLocalDate, LocalDate reSignDate, String taskId) {
        String activityId = participateActivityInfo.getActivityId();
        String userId = participateActivityInfo.getUserId();
        String appId = participateActivityInfo.getAppId();
        int reClockOffset = getOffset(joinLocalDate, reSignDate);
        // 查询是否打卡
        String signKey = activityClockRedisService.getUserActivityClockKey(activityId, userId, true);
        boolean hasSign = activityClockRedisService.getSign(signKey, reClockOffset);
        if (hasSign) {
            return true;
        }
        // 添加打卡
        activityClockRedisService.sign(signKey, reClockOffset);
        // 添加邀请人补卡状态
        String reSignKey = activityClockRedisService.getUserActivityClockKey(activityId, userId, false);
        activityClockRedisService.sign(reSignKey, reClockOffset);
        // 添加邀请人打卡记录
        UserClockRecord currentDayRecord = find(activityId, userId, reSignDate);
        if (currentDayRecord != null) {
            return false;
        }
        //邀请人的补卡记录
        UserClockRecord record = new UserClockRecord();
        record.setAppId(appId);
        record.setUserId(userId);
        record.setActivityId(activityId);
        record.setTaskId(taskId);
        record.setMarkType(1);
        record.setSignDate(reSignDate);
        record.setSignDateTime(LocalDateTime.now());
        mongoTemplate.insert(record);
        return false;
    }


    public UserClockRecord find(String activityId, String userId, LocalDate date) {
        Query query = new Query().addCriteria(Criteria.where("userId").is(userId).and("activityId").is(activityId).and("signDate").is(date));
        return mongoTemplate.findOne(query, UserClockRecord.class);
    }

    /**
     * 获取用户打卡信息
     *
     * @param activityId
     * @param userId
     * @param date
     * @return
     */
    public UserSignInfoDTO getSignInfo(String activityId, String userId, LocalDate date) {
        UserSignInfoDTO dto = new UserSignInfoDTO();
        //获取用户报名信息
        UserParticipateActivityInfo participateActivityInfo = userParticipateActivityInfoService.findByUserIdActivityId(userId, activityId);
        dto.setSignUpTime(participateActivityInfo.getSignUpTime());
        // 查询本周签到信息
        if (participateActivityInfo == null || !participateActivityInfo.getJoined()) {
            return dto;
        }
        Date signDate = participateActivityInfo.getSignUpTime();
        LocalDate signLocalDate = LocalDateTimeUtil.of(signDate).toLocalDate();
        int diff = getOffset(signLocalDate, date);
        if (diff < 0) {
            return dto;
        }
        String signKey = activityClockRedisService.getUserActivityClockKey(activityId, userId, true);
        int continueDay = diff + 1;
//        long signNum = getSignNum(signKey, 0, continueDay);
//        int signDay = getSignContinueCount(signNum, continueDay, true);
        int continueSignDay = doLoopSignContinue(continueDay, signKey,true);
        boolean isSign = activityClockRedisService.getSign(signKey, diff);
        dto.setCurrentDateSignIn(isSign);
        dto.setUserContinueSignInDay(continueSignDay);
        int totalSign = (int) activityClockRedisService.getSignCount(signKey);
        dto.setUserReSignInDay(continueDay - totalSign - (isSign ? 0 : 1));
        return dto;
    }

    public int getUserContinueSignInDay(String activityId, String userId, Date signUpTime) {
        if (signUpTime == null) {
            return 0;
        }
        LocalDate signLocalDate = LocalDateTimeUtil.of(signUpTime).toLocalDate();
        int diff = getOffset(signLocalDate, LocalDate.now());
        if (diff < 0) {
            return 0;
        }
        String signKey = activityClockRedisService.getUserActivityClockKey(activityId, userId, true);
        int continueDay = diff + 1;
        long signNum = getSignNum(signKey, 0, continueDay);
        int continueSignDay = getSignContinueCount(signNum, continueDay, true);
        return continueSignDay;
    }

    public UserClockRecord buildRecord(UserClockSaveDTO dto, LocalDateTime date) {
        UserClockRecord record = new UserClockRecord();
        record.setAppId(dto.getAppId());
        record.setUserId(dto.getUserId());
        record.setActivityId(dto.getActivityId());
        record.setMarkType(0);
        record.setSignDateTime(date);
        return record;
    }

    /**
     * 查询每周打卡数据
     *
     * @param dto
     * @return
     */
    public Object queryUserWeekClockInfo(UserClockQueryDTO dto) {
        String userId = dto.getUserId();
        String activityId = dto.getActivityId();
        LocalDate date = dto.getClockDate().toLocalDate();
        //获取用户报名信息
        UserParticipateActivityInfo participateActivityInfo = userParticipateActivityInfoService.findByUserIdActivityId(userId, activityId);
        // 查询本周签到信息
        if (participateActivityInfo == null || !participateActivityInfo.getJoined()) {
            throw new BusinessException("用户未报名");
        }
        Date signDate = participateActivityInfo.getSignUpTime();

        LocalDate signLocalDate = LocalDateTimeUtil.of(signDate).toLocalDate();
        // 查询本周
        List<LocalDate> dates = getCurrentWeekDate(date);
        return getSignData(activityId, userId, dates, signLocalDate);
    }

    /**
     * 查询每周月打卡数据
     *
     * @param dto
     * @return
     */
    public Object queryUserMonthClockInfo(UserClockQueryDTO dto) {
        String userId = dto.getUserId();
        String activityId = dto.getActivityId();
        LocalDate currentDate = LocalDate.now();
        LocalDate inputDate = getDateByStr(dto.getClockDateStr());
        //获取用户报名信息
        UserParticipateActivityInfo participateActivityInfo = userParticipateActivityInfoService.findByUserIdActivityId(userId, activityId);
        // 查询本周签到信息
        if (participateActivityInfo == null || !participateActivityInfo.getJoined()) {
            throw new BusinessException("用户未报名");
        }
        Date signDate = participateActivityInfo.getSignUpTime();
        LocalDate signLocalDate = LocalDateTimeUtil.of(signDate).toLocalDate();
        if (compareMonth(signLocalDate, inputDate) > 0) {
            throw new BusinessException("暂无数据");
        }
        if (compareMonth(currentDate, inputDate) < 0) {
            throw new BusinessException("暂无数据");
        }
        List<LocalDate> dates = getMonthDate(inputDate);
        return getSignData(activityId, userId, dates, signLocalDate);
    }


    public static int compareMonth(LocalDate date, LocalDate otherDate) {
        int num = date.getYear() * 100 + date.getMonthValue();
        int otherNum = otherDate.getYear() * 100 + otherDate.getMonthValue();
        return Integer.compare(num, otherNum);
    }

    /**
     * 根据日期获取不同签到组合结果
     *
     * @param activityId
     * @param userId
     * @param dates
     * @param signLocalDate
     * @return
     */
    public List<UserClockRecordVo> getSignData(String activityId, String userId, List<LocalDate> dates, LocalDate signLocalDate) {
        LocalDate start = dates.get(0);
        int dateSize = dates.size();
        LocalDate end = dates.get(dateSize - 1);
        if (signLocalDate.isAfter(end)) {
            throw new BusinessException("活动未开始");
        }
        int offset = 0;
        int signDataLength = dateSize;
        if (signLocalDate.isBefore(start)) {
            offset = getOffset(signLocalDate, start);
            List<Integer> signDataList = getSignDataList(activityId, userId, offset, signDataLength, true);
            List<Integer> reSignDataList = getSignDataList(activityId, userId, offset, signDataLength, false);
            return getSignDataList(dates, signDataList, reSignDataList);
        }
        long diff = getOffset(signLocalDate, end);
        signDataLength = (int) (diff + 1);
        List<Integer> signDataList = getSignDataList(activityId, userId, offset, signDataLength, true);
        List<Integer> reSignDataList = getSignDataList(activityId, userId, offset, signDataLength, false);
        return getSignDataList(dates, signDataList, reSignDataList);
    }

    /**
     * 根据总的签到结果解析签到列表
     *
     * @param activityId
     * @param userId
     * @param offset
     * @param signLength
     * @param sign
     * @return
     */
    public List<Integer> getSignDataList(String activityId, String userId, int offset, int signLength, boolean sign) {
        String signKey = activityClockRedisService.getUserActivityClockKey(activityId, userId, sign);
        long num = getSignNum(signKey, offset, signLength);
        List<Integer> signList = getSignList(num, signLength);
        return signList;
    }

    /**
     * 组合日期和签到结果
     *
     * @param dates
     * @param signList
     * @param reSignList
     * @return
     */
    public List<UserClockRecordVo> getSignDataList(List<LocalDate> dates, List<Integer> signList, List<Integer> reSignList) {
        List<UserClockRecordVo> list = new ArrayList<>();
        int dateSize = dates.size();
        int signSize = signList.size();
        int reSignSize = reSignList.size();
        int diff = dateSize - signSize;
        for (int i = 0; i < dateSize; i++) {
            LocalDate date = dates.get(i);
            int signIndex = i - diff;
            int status = signIndex < 0 ? 0 : signList.get(signIndex);
            if (status == 1 && reSignSize > 0 && reSignList.get(signIndex) == 1) {
                status = 2;
            }
            list.add(buildUserClockResult(date, status));
        }
        return list;
    }


    /**
     * 解析签到值
     *
     * @param signKey
     * @param offset
     * @param signLength
     * @return
     */
    public long getSignNum(String signKey, int offset, int signLength) {
        List<Long> numList = activityClockRedisService.getSignList(signKey, offset, signLength);
        if (CollectionUtils.isEmpty(numList)) {
            return 0;
        }
        return numList.get(0);
    }

    /**
     * 将签到值转化为连续签到数组
     *
     * @param signNum
     * @param signLength
     * @return
     */
    public List<Integer> getSignList(long signNum, int signLength) {
        if (signNum <= 0) {
            return new ArrayList<>();
        }
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < signLength; i++) {
            list.add(i, (int) (signNum & 1));
            signNum >>>= 1;
        }
        Collections.reverse(list);
        return list;
    }

    /**
     * 根据签到值计算连续打卡天数
     *
     * @param signNum
     * @param signLength
     * @param contain
     * @return
     */
    public int getSignContinueCount(long signNum, int signLength, boolean contain) {
        int total = 0;
        for (int i = signLength; i > 0; i--) {
            int status = (int) (signNum & 1);
            signNum >>>= 1;
            if (i != signLength) {
                if (status != 1) {
                    break;
                }
                total++;
            } else {
                if (status == 1 && contain) {
                    total++;
                }
            }
        }
        return total;
    }

    public static int getOffset(LocalDate start, LocalDate end) {
        long offset = start.until(end, ChronoUnit.DAYS);
        return (int) offset;
    }

    public UserClockRecordVo buildUserClockResult(LocalDate date, int status) {
        UserClockRecordVo vo = new UserClockRecordVo();
        vo.setStatus(status);
        vo.setSignDay(date.toString());
        return vo;
    }

    public List<UserClockRecordVo> buildData(String activityId, String userId, List<LocalDate> dates) {
        List<UserClockRecordVo> list = new ArrayList<>();
        for (int i = 0; i < dates.size(); i++) {
            list.add(buildUserClockResult(dates.get(i), 0));
        }
        return list;
    }

    public static List<LocalDate> getCurrentWeekDate(LocalDate date) {
        int dayofweek = date.getDayOfWeek().getValue();
        List<LocalDate> list = new ArrayList<>();
        LocalDate start = date.plusDays(-dayofweek + 1);
        for (int i = 0; i < 7; i++) {
            list.add(start.plusDays(i));
        }
        return list;
    }

    public static LocalDate getDateByStr(String dateStr) {
        if (StringUtils.isBlank(dateStr)) {
            return LocalDate.now();
        }
        return LocalDateTimeUtil.parseDate(dateStr, "yyyy-M-d");
    }

    public static List<LocalDate> getMonthDate(LocalDate date) {
        LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth());
        int firstDayWeek = firstDay.getDayOfWeek().getValue();
        LocalDate startDate = firstDay.minusDays(firstDayWeek - 1);
        LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
        int lastDayWeek = lastDay.getDayOfWeek().getValue();
        LocalDate endDate = lastDay.plusDays(7 - lastDayWeek);
        long diff = startDate.until(endDate, ChronoUnit.DAYS);
        List<LocalDate> list = Lists.newArrayList();
        for (int i = 0; i <= diff; i++) {
            list.add(startDate.plusDays(i));
        }
        return list;
    }

    public int doLoopSignContinue(int continueDay, String signKey) {
        int time = (continueDay + 31) / 32;
        int total = 0;
        for (int i = 1; i <= time; i++) {
            int offset = (time - 1) * 8;
            int end = Math.min(offset + 8, continueDay);
            int len = end - offset;
            long signNum = getSignNum(signKey, offset, end);
            int continueSignDay = getSignContinueCount(signNum, len, true);
            total += continueSignDay;
        }
        return total;
    }

    public int doLoopSignContinue(int continueDay, String signKey, boolean contain) {
        int continueNum = 32;
        int time = (continueDay + continueNum - 1) / continueNum;
        int total = 0;
        for (int i = time; i > 0; i--) {
            int offset = (i - 1) * continueNum;
            int end = Math.min(offset + continueNum, continueDay);
            int len = end - offset;
            long signNum = getSignNum(signKey, offset, len);
            for (int j = len; j > 0; j--) {
                int status = (int) (signNum & 1);
                signNum >>>= 1;
                if (i == time && j == len) {
                    if (status == 1 && contain) {
                        total++;
                    }
                } else {
                    if (status != 1) {
                        return total;
                    }
                    total++;
                }
            }
        }
        return total;
    }
}
